package com.idas.wsdl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.ws.Holder;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import com.idas.service.API;
import com.idas.wsdl.WSDLComment.COMMNT_TYPE;
import com.sun.xml.internal.ws.api.BindingID;
import com.sun.xml.internal.ws.api.wsdl.writer.WSDLGeneratorExtension;
import com.sun.xml.internal.ws.binding.WebServiceFeatureList;
import com.sun.xml.internal.ws.model.AbstractSEIModelImpl;
import com.sun.xml.internal.ws.model.JavaMethodImpl;
import com.sun.xml.internal.ws.model.ParameterImpl;
import com.sun.xml.internal.ws.model.RuntimeModeler;

import com.sun.xml.internal.ws.util.ServiceFinder;
import com.sun.xml.internal.ws.wsdl.writer.WSDLGenerator;
import com.sun.xml.internal.ws.wsdl.writer.WSDLResolver;

import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class WSDLGenDoc {

	private static DocumentBuilderFactory builderFacwsdl, builderFacxsd;

	static {
		builderFacwsdl = DocumentBuilderFactory.newInstance();
		builderFacxsd = DocumentBuilderFactory.newInstance();
		builderFacxsd.setNamespaceAware(true);
	}

	private Class serviceClass;

	// These two output streams will hold the generated data from WSDLGenerator
	private ByteArrayOutputStream wsdlGen, xsdGen;
	private ArrayList<String> include_packages;
	private ArrayList<String> classes_to_doument;

	private OutputStream wsdl, xsd;
	AbstractSEIModelImpl rtModel;

	public WSDLGenDoc(Object serviceObj, OutputStream hold_wsdl,
			OutputStream hold_xsd) {
		serviceClass = serviceObj.getClass();
		include_packages = new ArrayList<String>();
		classes_to_doument = new ArrayList<String>();
		wsdl = hold_wsdl;
		xsd = hold_xsd;

	}

	/**
	 * The complex type documentation will only be done on the classes which are
	 * included in these packages
	 * 
	 * @param package_name
	 */
	public void addDocIncludePackage(String package_name) {
		if (!include_packages.contains(package_name)) {
			include_packages.add(package_name);
		}
	}

	public void generateDocumentedWSDL() {
		wsdlGen = new ByteArrayOutputStream();
		xsdGen = new ByteArrayOutputStream();

		BindingID bindingID = BindingID.parse(serviceClass);
		RuntimeModeler rtModeler = new RuntimeModeler(serviceClass, null,
				bindingID);
		rtModeler.setClassLoader(serviceClass.getClassLoader());

		rtModel = rtModeler.buildRuntimeModel();

		WebServiceFeatureList wsfeatures = new WebServiceFeatureList(
				serviceClass);

		WSDLGenerator wsdlGenerator = new WSDLGenerator(rtModel,
				new WSDLResolver() {

					@Override
					public Result getAbstractWSDL(Holder<String> filename) {
						Result result = new StreamResult(wsdlGen);
						result.setSystemId(filename.value);
						return result;
					}

					@Override
					public Result getSchemaOutput(String namespace,
							Holder<String> suggestedFilename) {
						if (namespace.equals(""))
							return null;

						Result result = new StreamResult(xsdGen);
						result.setSystemId(suggestedFilename.value);
						return result;
					}

					@Override
					public Result getWSDL(String suggestedFilename) {
						Result result = new StreamResult(wsdlGen);
						result.setSystemId(suggestedFilename);
						return result;
					}

					// TODO pass correct impl's class name
				}, bindingID.createBinding(wsfeatures.toArray()), null,
				serviceClass, ServiceFinder.find(WSDLGeneratorExtension.class)
						.toArray());
		wsdlGenerator.doGeneration();

		// Lets read the two WSDL files as XML
		Document wsdlDoc = null;
		Document xsdDoc = null;

		try {
			DocumentBuilder buildwsdl = builderFacwsdl.newDocumentBuilder();
			DocumentBuilder buildxsd = builderFacxsd.newDocumentBuilder();

			wsdlDoc = buildwsdl.parse(new ByteArrayInputStream(wsdlGen
					.toByteArray()));
			xsdDoc = buildxsd.parse(new ByteArrayInputStream(xsdGen
					.toByteArray()));

			updateWithDoc(wsdlDoc, xsdDoc);
			serializeToOutPut(wsdlDoc, xsdDoc);
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	private void updateWithDoc(Document wsdlToUpdate, Document xsdToUpdate) {
		Method methods[] = serviceClass.getMethods();
		NodeList list = wsdlToUpdate.getChildNodes();
		Node def_node = null;
		for (int i = 0; i < list.getLength(); i++) {
			Node n = list.item(i);
			if (n.getNodeName().equals("definitions")) {
				def_node = n;
				// System.out.println(def_node.getNodeName());
				break;
			}
		}
		Node portType = getNodeToXpath(def_node, "./portType", false);
		if (portType != null) {

			for (Method method : methods) {

				WSDLComment comm = method.getAnnotation(WSDLComment.class);
				Annotation[][] methodInParamsAnn = method
						.getParameterAnnotations();
				Annotation[] methodOutAnn = method.getAnnotations();

				if (comm != null) {
					Node opNode = getNodeToXpath(portType,
							"./operation[@name='" + method.getName() + "']",
							false);
					if (opNode != null) {
						COMMNT_TYPE type = comm.type();
						if (type == COMMNT_TYPE.WSDL_DOC) {
							// Main comment on the WSDL file
							if (comm.comment().length() > 0) {
								Node comment_node = createWSDLDocAnnotation(
										wsdlToUpdate, comm.comment());
								opNode.insertBefore(comment_node,
										opNode.getFirstChild());
							}
							// Comment on the input mgs
							// if (comm.inputcomment().length() > 0) {
							Node in_node = getNodeToXpath(
									xsdToUpdate.getDocumentElement(),
									"./xs:complexType[@name='"
											+ method.getName() + "']", true);
							Node ann = createXSDDocAnnotation(
									xsdToUpdate,
									"Input parameters for method "
											+ method.getName());
							in_node.insertBefore(ann, in_node.getFirstChild());
							// Sort out the input parameters
							for (int i = 0; i < methodInParamsAnn.length; i++) {
								int x = methodInParamsAnn[i].length;
								String param_name = null, param_doc = null;
								for (int j = 0; j < x; j++) {
									Annotation a = methodInParamsAnn[i][j];
									if (a.annotationType().equals(
											WebParam.class)) {
										param_name = ((WebParam) a).name();
									}
									if (a.annotationType().equals(
											XmlDocumentation.class)) {
										param_doc = ((XmlDocumentation) a)
												.value();
									}

								}
								if (param_name != null && param_doc != null) {
									Node param_node = getNodeToXpath(in_node,
											".//xs:element[@name='"
													+ param_name + "']", true);
									if (param_node != null) {
										ann = createXSDDocAnnotation(
												xsdToUpdate, param_doc);
										param_node.appendChild(ann);
									}
								}
							}

							// }

							// Comment on output msg
							// if (comm.outputcomment().length() > 0) {
							Node out_node = getNodeToXpath(
									xsdToUpdate.getDocumentElement(),
									"./xs:complexType[@name='"
											+ method.getName() + "Response']",
									true);
							ann = createXSDDocAnnotation(
									xsdToUpdate,
									"Output result for method "
											+ method.getName());
							out_node.insertBefore(ann, out_node.getFirstChild());
							String res_name = null, res_doc = null;
							for (int i = 0; i < methodOutAnn.length; i++) {
								Annotation a = methodOutAnn[i];

								if (a.annotationType().equals(WebResult.class)) {
									res_name = ((WebResult) a).name();
								}

								if (a.annotationType().equals(
										XmlDocumentation.class)) {
									res_doc = ((XmlDocumentation) a).value();
								}
							}
							if (res_name != null && res_doc != null) {
								Node res_node = getNodeToXpath(out_node,
										".//xs:element[@name='" + res_name
												+ "']", true);
								if (res_node != null) {
									ann = createXSDDocAnnotation(xsdToUpdate,
											res_doc);
									res_node.appendChild(ann);
								}
							}

							// }

							if (comm.faultcomment().length() > 0) {
								Node fault = getNodeToXpath(opNode, "./fault",
										false);
								if (fault != null) {
									Node fa = createWSDLDocAnnotation(
											wsdlToUpdate, comm.faultcomment());
									fault.appendChild(fa);
								}
							}
						}
					}
				}
				buildComlexTypeList(method);
				handleInputParams(method, null);
			}
			// Now go though the class List finding the annotation
			Iterator<String> ite = classes_to_doument.iterator();
			ClassLoader loder = serviceClass.getClassLoader();

			while (ite.hasNext()) {
				String className = ite.next();
				try {
					Class clz = loder.loadClass(className);
					XmlDocumentation docanno = (XmlDocumentation) clz
							.getAnnotation(XmlDocumentation.class);

					String simname = clz.getSimpleName();

					Node edid_type = getNodeToXpath(
							xsdToUpdate.getDocumentElement(),
							"./xs:complexType[translate(@name,'abcdefghijklmnopqrstuvwxyz',"
									+ "'ABCDEFGHIJKLMNOPQRSTUVWXYZ')='"
									+ simname.toUpperCase() + "']", true);
					if (edid_type == null) {
						edid_type = getNodeToXpath(
								xsdToUpdate.getDocumentElement(),
								"./xs:simpleType[translate(@name,'abcdefghijklmnopqrstuvwxyz',"
										+ "'ABCDEFGHIJKLMNOPQRSTUVWXYZ')='"
										+ simname.toUpperCase() + "']", true);
					}
					if (edid_type != null) {
						if (docanno != null && docanno.value().length() > 0) {
							Node doc_node = createXSDDocAnnotation(xsdToUpdate,
									docanno.value());
							edid_type.insertBefore(doc_node,
									edid_type.getFirstChild());
						}
						Field fields[] = clz.getDeclaredFields();
						for (Field field : fields) {
							XmlDocumentation fieldanno = field
									.getAnnotation(XmlDocumentation.class);
							if (fieldanno != null
									&& fieldanno.value().length() > 0) {
								Node xsd_field = getNodeToXpath(
										edid_type,
										".//xs:element[@name='"
												+ field.getName() + "']", true);
								if (xsd_field != null) {
									Node doc_node = createXSDDocAnnotation(
											xsdToUpdate, fieldanno.value());
									xsd_field.insertBefore(doc_node,
											xsd_field.getFirstChild());
								}
							}

						}
					} else {
						// System.out.println("The "+className+
						// " Not found in xsd");
					}

				} catch (ClassNotFoundException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			}
		}

	}

	private void buildComlexTypeList(Method method) {
		Class input[] = method.getParameterTypes();
		Type inTypes[] = method.getGenericParameterTypes();

		Class output = method.getReturnType();

		Type outType = method.getGenericReturnType();

		Class excep[] = method.getExceptionTypes();

		System.out.println("Handling Mathod " + method.getName());

		for (int i = 0; i < inTypes.length; i++) {
			Type intype = inTypes[i];
			if (intype instanceof ParameterizedType) {
				ParameterizedType pt = (ParameterizedType) intype;
				Type t = pt.getActualTypeArguments()[0];
				if (t instanceof Class) {
					System.out.println("Input Param Genaric Class : "
							+ ((Class) t).getName() + " ###### ");
					buildClassFieldList((Class) t);
				}
			} else {
				System.out.println("Input Param Normal Class : "
						+ input[i].getName() + " ###### ");
				buildClassFieldList(input[i]);
			}

		}

		/*
		 * for (Class class1 : input) { System.out.println(class1.getName() +
		 * " ###### " + class1.getComponentType()); buildClassFieldList(class1);
		 * }
		 */
		if (outType instanceof ParameterizedType) {
			ParameterizedType pt = (ParameterizedType) outType;
			Type t = pt.getActualTypeArguments()[0];
			if (t instanceof Class) {
				System.out.println("Output Genaric Class : "
						+ ((Class) t).getName() + " ###### ");
				buildClassFieldList((Class) t);
			}
		} else {
			System.out.println("Output : " + output.getName() + " ###### ");
			buildClassFieldList(output);
		}

		for (Class class1 : excep) {
			buildClassFieldList(class1);
		}
		System.out.println("\n\n\n");
	}

	private void buildClassFieldList(Class clz) {
		if (clz != null) {
			if (!classes_to_doument.contains(clz.getName())) {
				if (classNeedToDocument(clz.getName())) {
					// System.out.println("Building calss : "+clz.getName());
					classes_to_doument.add(clz.getName());
					buildClassFieldList(clz.getSuperclass());
					Field[] fileds = clz.getDeclaredFields();
					for (Field field : fileds) {
						Type ty = field.getGenericType();
						if (ty instanceof ParameterizedType) {
							ParameterizedType pt = (ParameterizedType) ty;
							Type t = pt.getActualTypeArguments()[0];
							if (t instanceof Class) {
								Class c = (Class) t;
								if (classNeedToDocument(c.getName())) {
									buildClassFieldList(c);
								}
							}
						} else {
							if (classNeedToDocument(field.getType().getName())) {
								buildClassFieldList(field.getType());
							}
						}

					}
				}
			}
		}
	}

	private void handleInputParams(Method method, Node complexType) {
		JavaMethodImpl jm = rtModel.getJavaMethod(method);
		if (jm != null) {
			List<ParameterImpl> params = jm.getRequestParameters();
			for (ParameterImpl parameterImpl : params) {
				// System.out.println(parameterImpl.getName().getLocalPart());
			}
		}
		/*
		 * Annotation[][] annotation_list = method.getParameterAnnotations();
		 * if(annotation_list.length>0){
		 * 
		 * }
		 */
	}

	private boolean classNeedToDocument(String className) {
		if (include_packages.size() > 0) {
			for (String type : include_packages) {
				if (className.startsWith(type)) {
					return true;
				}
			}
		} else {
			return true;
		}
		return false;
	}

	private void serializeToOutPut(Document wsdlDoc, Document xsdDoc)
			throws IOException {
		if (wsdl != null) {
			Source sourcewsdl = new DOMSource(wsdlDoc);
			StreamResult reswsdl = new StreamResult(new StringWriter());

			try {
				Transformer xformer = TransformerFactory.newInstance()
						.newTransformer();
				xformer.transform(sourcewsdl, reswsdl);
				wsdl.write(reswsdl.getWriter().toString().getBytes());
			} catch (TransformerConfigurationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (TransformerFactoryConfigurationError e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (TransformerException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		if (xsd != null) {
			Source sourcewsdl = new DOMSource(xsdDoc);
			StreamResult reswsdl = new StreamResult(new StringWriter());

			try {
				Transformer xformer = TransformerFactory.newInstance()
						.newTransformer();
				xformer.transform(sourcewsdl, reswsdl);
				xsd.write(reswsdl.getWriter().toString().getBytes());
			} catch (TransformerConfigurationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (TransformerFactoryConfigurationError e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (TransformerException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

	}

	public static void main(String args[]) {

		final File[] wsdlFileName = new File[1]; // used to capture the
													// generated WSDL file.
		final Map<String, File> schemaFiles = new HashMap<String, File>();

		String directory = "/tmp";
		if (args.length > 0) {
		    directory = args[0];
		}

		API ser = new API();
		File f = new File(directory + "/AdminAPI.wsdl");
		File f2 = new File(directory + "/AdminAPI_schema1.xsd");
		try {
			f.createNewFile();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		FileOutputStream fiowsdl = null;
		FileOutputStream fioxsd = null;
		try {
			fiowsdl = new FileOutputStream(f);
			fioxsd = new FileOutputStream(f2);
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		WSDLGenDoc docgen = new WSDLGenDoc(ser, fiowsdl, fioxsd);
		docgen.addDocIncludePackage("com.extl.jade.comp");
		docgen.generateDocumentedWSDL();
		try {
			fiowsdl.flush();
			fiowsdl.close();
			fioxsd.flush();
			fioxsd.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	private static Node createWSDLDocAnnotation(Document doc, String comment) {

		Node doc_node = doc.createElement("documentation");
		doc_node.setTextContent(comment);
		return doc_node;

	}

	private static Node createXSDDocAnnotation(Document doc, String comment) {
		Node annotaion = doc.createElement("xs:annotation");
		Node doc_a = doc.createElement("xs:documentation");
		doc_a.setTextContent(comment);
		annotaion.appendChild(doc_a);
		return annotaion;
	}

	private static Node getNodeToXpath(Node rootNode, String xpath, boolean xsd) {
		Node pathNode = null;
		XPath pathobject = XPathFactory.newInstance().newXPath();
		if (xsd) {
			pathobject.setNamespaceContext(new NamespaceContext() {
				public String getNamespaceURI(String prefix) {
					if (prefix.equals("xs")) {
						// //System.out.println("getNamespaceURI " + prefix);
						return "http://www.w3.org/2001/XMLSchema";
					}
					if (prefix.equals("tns")) {
						return "http://extility.flexiant.net";
					}
					return "";
				}

				public String getPrefix(String namespaceURI) {

					if (namespaceURI.equals("http://www.w3.org/2001/XMLSchema")) {
						// //System.out.println("getPrefix " + namespaceURI);
						return "xs";
					}
					if (namespaceURI.equals("http://extility.flexiant.net")) {
						return "tns";
					}
					return "";
				}

				public Iterator getPrefixes(String namespaceURI) {
					List<String> list = new ArrayList<String>();

					if (namespaceURI.equals("http://www.w3.org/2001/XMLSchema")) {
						// //System.out.println("getPrefixes " + namespaceURI);
						list.add("xs");
					}
					if (namespaceURI.equals("http://extility.flexiant.net")) {
						list.add("tns");
					}

					return list.iterator();
				}
			});
		}
		/*
		 * if(xsd){ pathobject.se }
		 */
		try {
			// //System.out.println(" XPATH " + xpath);
			XPathExpression expr = pathobject.compile(xpath);
			// NodeList obj = (NodeList)
			// expr.evaluate(rootNode,XPathConstants.NODESET);
			NodeList obj = (NodeList) pathobject.evaluate(xpath, rootNode,
					XPathConstants.NODESET);
			// //System.out.println(" XPATH " + xpath + " Object " +
			// obj.toString()
			// + " " + obj.getLength());
			pathNode = obj.item(0);
		} catch (XPathExpressionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return pathNode;
	}

}
